#!/usr/bin/env ruby
require 'find'
require 'net/http'
require 'uri'
require 'digest/sha1'
require 'json'
require 'fileutils'

@command = ARGV[0]
@pkgName = ARGV[1]

CREW_PREFIX = '/usr/local'
CREW_LIB_PATH = CREW_PREFIX + '/lib/crew/'
CREW_CONFIG_PATH = CREW_PREFIX + '/etc/crew/'
CREW_BREW_DIR = CREW_PREFIX + '/tmp/crew/'
CREW_DEST_DIR = CREW_BREW_DIR + '/dest'

SHORTARCH = `getconf LONG_BIT | tr -d '\n\r'`

$LOAD_PATH.unshift "#{CREW_LIB_PATH}lib"

USER = `whoami`.chomp

@device = JSON.parse(File.read(CREW_CONFIG_PATH + 'device.json'), symbolize_names: true)
#symbolize also values
@device.each do |key, elem|
  @device[key] = @device[key].to_sym rescue @device[key]
end

def setPkg (pkgName, silent = false)
  require CREW_LIB_PATH + 'packages/' + pkgName
  @pkg = Object.const_get(pkgName.capitalize)
  @pkg.name = pkgName
  puts "Found #{pkgName}, version #{@pkg.version}" unless silent
end

def list_packages
  Find.find (CREW_LIB_PATH + 'packages') do |filename|
    Find.find(CREW_CONFIG_PATH + 'meta/') do |packageList|
        packageName = File.basename filename, '.rb'
        print '(i) ' if packageList == CREW_CONFIG_PATH + 'meta/' + packageName + '.filelist' 
    end
    puts File.basename filename, '.rb' if File.extname(filename) == '.rb'
  end
end

def search (pkgName, silent = false)
  Find.find (CREW_LIB_PATH + 'packages') do |filename|
    return setPkg(pkgName, silent) if filename == CREW_LIB_PATH + 'packages/' + pkgName + '.rb'
  end
  abort "package #{pkgName} not found :("
end

def update
  abort "'crew update' is used to update crew itself. Use 'crew upgrade <packageName> to upgrade a specific package." if @pkgName

  #update package lists
  Dir.chdir CREW_LIB_PATH do
    system "git fetch origin master"
    system "git reset --hard origin/master"
  end
  puts "Package lists, crew, and library updated."
  
  #check for outdated installed packages
  puts "Checking for package updates..."
  puts ""

  canBeUpdated = 0
  @device[:installed_packages].each do |package|
    search package[:name], true
    if package[:version] != @pkg.version
      canBeUpdated += 1
      puts @pkg.name + " could be updated from " + package[:version] + " to " + @pkg.version
    end
  end
  
  if canBeUpdated > 0
    puts ""
    puts "Run 'crew upgrade' to upgrade everything or 'crew upgrade <packageName>' to upgrade a specific package."
  else
    puts "Your software is up to date."
  end
end

def upgrade
  if @pkgName
    search @pkgName

    currentVersion = nil
    @device[:installed_packages].each do |package|
      if package[:name] == @pkg.name
        currentVersion = package[:version]
      end
    end
    
    if currentVersion != @pkg.version
      search @pkg.name
      puts "Updating #{@pkg.name}..."
      remove 
      resolveDependenciesAndInstall
    else
      puts "#{@pkg.name} is already up to date."
    end
  else
    toBeUpdated = []
    @device[:installed_packages].each do |package|
      search package[:name], true
      if package[:version] != @pkg.version
        toBeUpdated.push(package[:name])
      end
    end
  
    if toBeUpdated.length > 0
      puts "Updating packages..."
      toBeUpdated.each do |package|
        search package
        remove
        resolveDependenciesAndInstall
      end
      puts "Packages have been updated."
    else
      puts "Your software is already up to date."
    end
  end
end

def download
  if @pkg.binary_url && @pkg.binary_url.has_key?(@device[:architecture])
    url = @pkg.binary_url[@device[:architecture]]
    source = false
    puts "Precompiled binary available, downloading..."
  else
    url = @pkg.source_url
    source = true
    puts "No precompiled binary available for your platform, downloading source..."
  end
  uri = URI.parse url
  filename = File.basename(uri.path)
  if source
    sha1sum = @pkg.source_sha1
  else
    sha1sum = @pkg.binary_sha1[@device[:architecture]]
  end
  Dir.chdir CREW_BREW_DIR do
    system('wget', '--continue', '--content-disposition', '--no-check-certificate', '-N', url, '-O', filename)
    abort 'Checksum mismatch :/ try again' unless Digest::SHA1.hexdigest( File.read("./#{filename}") ) == sha1sum
  end
  puts "Archive downloaded"
  return {source: source, filename: filename}
end

def resolveDependenciesAndInstall
  begin
    origin = @pkg.name

    resolveDependencies
  
    search origin
    install
  rescue InstallError => e 
    puts "#{@pkg.name} failed to install: #{e.to_s}"
  ensure
    #cleanup
    unless ARGV[2] == 'keep'
      Dir.chdir CREW_BREW_DIR do
        system "rm -rf *"
        system "mkdir dest" #this is a little ugly, feel free to find a better way
      end
    end
  end
end

def resolveDependencies
  @dependencies = []

  def pushDeps
    if @pkg.dependencies
      @dependencies.unshift @pkg.dependencies
      
      @pkg.dependencies.each do |dep|
        search dep, true
        pushDeps
      end
    end
  end
  
  pushDeps
  
  return if @dependencies.empty?

  puts "Following packages also need to be installed: "
  
  @dependencies.flatten!.uniq!
  @dependencies.each do |dep|
    print dep + " "
  end
  
  puts ""
  puts "Do you agree? [Y/n]"
  response = STDIN.getc
  case response
  when "n"
    abort "No changes made."
  when "\n", "y", "Y"
    puts "Proceeding..."
    proceed = true
  else
    puts "I don't understand '#{response}' :("
    abort "No changes made."
  end

  if proceed
    @dependencies.each do |dep|
      search dep
      install
    end
  end
end

def install
  if @device[:installed_packages].any? { |pkg| pkg[:name] == @pkg.name }
    puts "Package #{@pkg.name} already installed, skipping..."
    return
  end

  unless @pkg.is_fake?
    meta = download
    Dir.chdir CREW_BREW_DIR do
      puts "Unpacking archive, this may take a while..."
      system "tar", "xf", meta[:filename]
      if meta[:source] == true
        abort "You don't have a working C compiler. Run 'crew install buildessential' to get one and try again." unless system("gcc", "--version")
        topdir = `tar -tf #{meta[:filename]} | sed -e 's@/.*@@' | uniq`.chomp!
        Dir.chdir CREW_BREW_DIR + topdir do
          puts "Building from source, this may take a while..."
          @pkg.build
          system "rm -rf", CREW_DEST_DIR + "/*" #wipe crew destdir
          puts "Preconfiguring package..."
          @pkg.install
        end

        Dir.chdir CREW_DEST_DIR do
          #create directory list
          system "find . -type f > ../filelist"
          system "find . -type l >> ../filelist"
          system "cut -c2- ../filelist > filelist"
          #create file list
          system "find . -type d > ../dlist"
          system "cut -c2- ../dlist > dlistcut"
          system "tail -n +2 dlistcut > dlist"
          #remove temporary files
          system "rm dlistcut ../dlist ../filelist"
          
          #create resulting folders list
          directories = []
          Dir.glob('*').select do |fn|
            if File.directory?(fn)
              directories.push(fn)
            end
          end

          #wipe directories with the same name in brew dir to avoid conflicts
          directories.each do |dir|
            system "rm -rf ../" + dir.chomp
          end

          #move result files and directories to brew dir
          system "mv * ../"
        end
      end

      puts "Installing..."

      FileUtils.mv 'dlist', CREW_CONFIG_PATH + "meta/#{@pkg.name}.directorylist"
      FileUtils.mv 'filelist', CREW_CONFIG_PATH + "meta/#{@pkg.name}.filelist"
 
      File.open(CREW_CONFIG_PATH + "meta/#{@pkg.name}.directorylist").each_line do |line|
        system "sudo", "mkdir", "-p", line.chomp
      end

      File.open(CREW_CONFIG_PATH + "meta/#{@pkg.name}.filelist").each_line do |line|
        system "sudo", "mv", CREW_BREW_DIR + line.chomp, line.chomp
      end
    end
  end
  
  #add to installed packages
  @device[:installed_packages].push(name: @pkg.name, version: @pkg.version)
  File.open(CREW_CONFIG_PATH + 'device.json', 'w') do |file|
    output = JSON.parse @device.to_json
    file.write JSON.pretty_generate(output)
  end
  puts "#{@pkg.name.capitalize} installed!"
end

def remove
  unless @device[:installed_packages].any? { |pkg| pkg[:name] == @pkg.name }
    puts "Package #{@pkg.name} isn't installed."
    return
  end
  
  unless @pkg.is_fake?
    Dir.chdir CREW_CONFIG_PATH do
      File.open("meta/#{@pkg.name}.filelist").each_line do |line|
        begin
          File.unlink line.chomp
        rescue => exception #swallow exception
        end
      end
  
      File.readlines("meta/#{@pkg.name}.directorylist").reverse.each do |line|
        begin
          Dir.rmdir line.chomp
        rescue => exception #swallow exception
        end
      end

      File.unlink "meta/#{@pkg.name}.filelist"
      File.unlink "meta/#{@pkg.name}.directorylist"
    end
  end

  #remove from installed packages
  @device[:installed_packages].each do |elem|
    @device[:installed_packages].delete elem if elem[:name] == @pkg.name
  end
  File.open(CREW_CONFIG_PATH + 'device.json', 'w') do |file|
    out = JSON.parse @device.to_json
    file.write JSON.pretty_generate(out)
  end
  puts "#{@pkg.name.capitalize} removed!"
end

case @command
when "search"
  if @pkgName
    search @pkgName
  else
    list_packages
  end
when "download"
  search @pkgName
  download
when "update"
  update
when "upgrade"
  upgrade
when "install"
  search @pkgName
  resolveDependenciesAndInstall
when "remove"
  abort 'Removing actions must be ran with sudo.' unless USER == 'root'
  search @pkgName
  remove
when nil
  puts "Chromebrew, version 0.2.1"
  puts "Usage: crew [command] [package]"
  puts "Available commands: search, download, install, remove"
else
  puts "I have no idea how to do #{@command} :("
  puts "Available commands: search, download, install, remove"
end
